package sk.upjs.ics.fca.bonds.application;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;

import sk.upjs.ics.fca.concepts.ClosureType;
import sk.upjs.ics.fca.concepts.Concept;
import sk.upjs.ics.fca.concepts.Context;
import sk.upjs.ics.fca.concepts.Logic;

/**
 * Tato trieda zahrna vsetky zalezitosti tykajuce sa nasho modeloveho prikladu,
 * kde aplikujeme nase poznatky o putach na priklad z praxe, kde identifikujeme
 * spravnych kandidatov pre dane univerzity a spravne univerzity pre danych
 * studentov, pricom nasou snahou je cim vacsia spravodlivost rozdelenia.
 * 
 * @author Patrik Mihalcin
 */
public class AlmightyAlgorithm {

	private Context[][] globalContext = null;
	private Context[] objectContexts = null;
	private Context[] attributeContexts = null;
	private Integer logicDegree = 0;
	private Logic logic = null;
	private Integer globalContextObjectsCount = 0;
	private Integer globalContextAttributesCount = 0;
	private List<List<Concept>> objectTuples = new ArrayList<List<Concept>>();
	private List<List<Concept>> attributeTuples = new ArrayList<List<Concept>>();

	/**
	 * Konstruktor prijima ako vstup 2 subory s info o zakladnom kontexte a
	 * externych kontextoch a stupen pouzitej fuzzy logiky
	 * 
	 * @param globalContextDataFile
	 *            prvy subor, kde je uvedene info o zakladnom kontexte
	 * @param inputContextsDataFile
	 *            druhy subor, kde je uvedene info o externych kontextoch
	 * @param logicDegree
	 *            aka fuzzy logika bude pouzita, napr. 3-hodnotova
	 */
	public AlmightyAlgorithm(File globalContextDataFile,
			File inputContextsDataFile, Integer logicDegree) {

		this.logicDegree = logicDegree;

		getGlobalContextData(globalContextDataFile);

		getInputContextsData(inputContextsDataFile);

		globalContextObjectsCount = globalContext.length;
		globalContextAttributesCount = globalContext[0].length;
	}

	private void getGlobalContextLengths(File dataFile) {
		Scanner sc = null;

		int k = 0;
		int l = 0;

		try {
			sc = new Scanner(dataFile);

			while (sc.hasNextLine()) {
				String line = sc.nextLine();

				if (line.toLowerCase().startsWith("context")) {
					String[] values = line.split(" ");
					for (int i = 0; i < values.length; i++) {
						if (values[i].toLowerCase().equals("o")) {
							k = Integer.parseInt(values[i + 1]);
						}
						if (values[i].toLowerCase().equals("a")) {
							l = Integer.parseInt(values[i + 1]);
						}
					}
				}
			}

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			sc.close();
		}

		globalContext = new Context[k][l];
	}

	private void getGlobalContextData(File dataFile) {

		getGlobalContextLengths(dataFile);

		Scanner sc = null;

		try {
			sc = new Scanner(dataFile);
			int j = 0;
			int k = 0;
			int l = 0;
			List<List<Double>> relation = new ArrayList<List<Double>>();

			do {
				String line = sc.nextLine();

				if (line.toLowerCase().startsWith("context")) {
					// get indices
					relation = new ArrayList<List<Double>>();
					j = 0;
					String[] values = line.split(" ");
					for (int i = 0; i < values.length; i++) {
						if (values[i].toLowerCase().equals("o")) {
							k = Integer.parseInt(values[i + 1]) - 1;
						}
						if (values[i].toLowerCase().equals("a")) {
							l = Integer.parseInt(values[i + 1]) - 1;
						}
					}
				} else {
					// build the relation
					String[] values = line.split(" ");
					relation.add(new ArrayList<Double>());

					for (int i = 0; i < values.length; i++) {
						relation.get(j).add(Double.parseDouble(values[i]));
					}
					j++;
				}

				if (!relation.isEmpty()) {
					globalContext[k][l] = new Context(logicDegree, relation);
				}
			} while (sc.hasNextLine());

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			sc.close();
		}
	}

	private void getInputContextsLengths(File dataFile) {
		Scanner sc = null;

		int k = 0;
		int l = 0;

		try {
			sc = new Scanner(dataFile);

			while (sc.hasNextLine()) {
				String line = sc.nextLine();

				if (line.toLowerCase().startsWith("context")) {
					String[] values = line.split(" ");
					for (int i = 0; i < values.length; i++) {
						if (values[i].toLowerCase().equals("o")) {
							k = Integer.parseInt(values[i + 1]);
						}
						if (values[i].toLowerCase().equals("a")) {
							l = Integer.parseInt(values[i + 1]);
						}
					}
				}
			}

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			sc.close();
		}

		objectContexts = new Context[k];
		attributeContexts = new Context[l];
	}

	private void getInputContextsData(File dataFile) {

		getInputContextsLengths(dataFile);

		Scanner sc = null;

		try {
			sc = new Scanner(dataFile);
			int j = 0;
			int k = 0;
			int l = 0;
			boolean objectRelationWasBuilt = false;
			boolean attributeRelationWasBuilt = false;
			List<List<Double>> relation = new ArrayList<List<Double>>();

			do {
				String line = sc.nextLine();

				if (line.toLowerCase().startsWith("context")) {
					relation = new ArrayList<List<Double>>();
					j = 0;

					// get indices
					String[] values = line.split(" ");
					for (int i = 0; i < values.length; i++) {
						if (values[i].toLowerCase().equals("o")) {
							k = Integer.parseInt(values[i + 1]) - 1;
							objectRelationWasBuilt = true;
							attributeRelationWasBuilt = false;
						}
						if (values[i].toLowerCase().equals("a")) {
							l = Integer.parseInt(values[i + 1]) - 1;
							objectRelationWasBuilt = false;
							attributeRelationWasBuilt = true;
						}
					}
				} else {
					// build the relation
					String[] values = line.split(" ");
					relation.add(new ArrayList<Double>());

					for (int i = 0; i < values.length; i++) {
						relation.get(j).add(Double.parseDouble(values[i]));
					}
					j++;
				}

				if (!relation.isEmpty()) {
					// add new context to the list
					if (objectRelationWasBuilt) {
						objectContexts[k] = new Context(logicDegree, relation);
					} else if (attributeRelationWasBuilt) {
						attributeContexts[l] = new Context(logicDegree,
								relation);
					}
				}
			} while (sc.hasNextLine());

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			sc.close();
		}
	}

	// private List<List<Double>> toList(Double[][] array) {
	//
	// List<List<Double>> list = new ArrayList<List<Double>>();
	//
	// for (int i = 0; i < array.length; i++) {
	// list.add(new ArrayList<Double>());
	// for (int j = 0; j < array[0].length; j++) {
	// list.get(i).add(array[i][j]);
	// }
	// }
	//
	// return list;
	// }

	private List<List<Double>> vectorToRelation(List<Double> vector,
			Integer objectsCount, Integer attributesCount) {
		List<List<Double>> relation = new ArrayList<List<Double>>();

		if (objectsCount * attributesCount != vector.size()) {
			System.err.println("Objects count = " + objectsCount
					+ " * attributes count = " + attributesCount
					+ " is not equal to the size of vector " + vector.size());
		}

		int p = 0;
		for (int i = 0; i < objectsCount; i++) {
			relation.add(new ArrayList<Double>());
			for (int j = 0; j < attributesCount; j++) {
				relation.get(i).add(vector.get(p++));
			}
		}

		return relation;
	}

	private List<Concept> specialDown(List<Concept> concepts) {
		List<Concept> result = new ArrayList<Concept>();

		for (int i = 0; i < globalContextObjectsCount; i++) {
			Context contextO = objectContexts[i];

			List<Double> infimum = new ArrayList<Double>();
			for (int j = 0; j < contextO.getAttributesCount(); j++) {
				infimum.add(1.0);
			}

			for (int j = 0; j < globalContextAttributesCount; j++) {
				// do direct product
				Context contextA = attributeContexts[j];
				Context directProduct = directProduct(contextO, contextA);

				List<Double> vector = getVectorOfObjects(this.globalContext[i][j]
						.getRelation());
				int objectsCount = this.globalContext[i][j].getRelation().length;
				int attributesCount = this.globalContext[i][j].getRelation()[0].length;
				// do closure
				List<Double> bondVector = closureOfVectorOfObjects(vector,
						directProduct, ClosureType.UP_DOWN);
				// vector -> relation
				List<List<Double>> bond = vectorToRelation(bondVector,
						objectsCount, attributesCount);

				List<Double> prechodR = prechodR(bond, concepts.get(j),
						contextO, contextA);
				infimum = infimum(infimum, prechodR);
			}

			result.add(new Concept(contextO.down(infimum), infimum));
		}

		return result;
	}

	private List<Concept> specialUp(List<Concept> concepts) {
		List<Concept> result = new ArrayList<Concept>();

		for (int i = 0; i < globalContextAttributesCount; i++) {
			Context contextA = attributeContexts[i];
			List<Double> infimum = new ArrayList<Double>();
			for (int j = 0; j < contextA.getObjectsCount(); j++) {
				infimum.add(1.0);
			}

			for (int j = 0; j < globalContextObjectsCount; j++) {
				// do direct product
				Context contextO = objectContexts[j];
				Context directProduct = directProduct(contextO, contextA);

				List<Double> vector = getVectorOfObjects(this.globalContext[j][i]
						.getRelation());
				int objectsCount = this.globalContext[j][i].getRelation().length;
				int attributesCount = this.globalContext[j][i].getRelation()[0].length;
				// do closure
				List<Double> bondVector = closureOfVectorOfObjects(vector,
						directProduct, ClosureType.UP_DOWN);
				// vector -> relation
				List<List<Double>> bond = vectorToRelation(bondVector,
						objectsCount, attributesCount);

				List<Double> prechodL = prechodL(bond, concepts.get(j),
						contextO, contextA);
				infimum = infimum(infimum, prechodL);
			}

			result.add(new Concept(infimum, contextA.up(infimum)));
		}

		return result;
	}

	private List<Double> infimum(List<Double> fuzzySet1, List<Double> fuzzySet2) {
		List<Double> result = new ArrayList<Double>();
		for (int i = 0; i < fuzzySet1.size(); i++) {
			result.add(Math.min(fuzzySet1.get(i), fuzzySet2.get(i)));
		}
		return result;
	}

	private List<Double> prechodR(List<List<Double>> bond, Concept concept,
			Context contextO, Context contextA) {
		List<Double> fuzzySetOfAttributesB = new ArrayList<Double>();
		List<Double> intent = concept.getIntent();
		List<List<Double>> rightCorrespondence = rightCorrespondence(bond,
				contextO, contextA);

		for (int i = 0; i < contextO.getAttributesCount(); i++) {
			double pom = 0.0;

			for (int j = 0; j < contextA.getAttributesCount(); j++) {
				Double conjunction = logic.strongConjunction(
						rightCorrespondence.get(j).get(i), intent.get(j));
				pom = Math.max(pom, conjunction);
			}
			fuzzySetOfAttributesB.add(pom);
		}
		return contextO.closure(fuzzySetOfAttributesB, ClosureType.DOWN_UP);
	} // vrati intent

	private List<Double> prechodL(List<List<Double>> bond, Concept concept,
			Context contextO, Context contextA) {
		List<Double> fuzzySetOfObjectsA = new ArrayList<Double>();
		List<Double> extent = concept.getExtent();
		List<List<Double>> leftCorrespondence = leftCorrespondence(bond,
				contextO, contextA);

		for (int i = 0; i < contextA.getObjectsCount(); i++) {
			double pom = 0.0;

			for (int j = 0; j < contextO.getObjectsCount(); j++) {
				Double conjunction = logic.strongConjunction(leftCorrespondence
						.get(j).get(i), extent.get(j));
				pom = Math.max(pom, conjunction);
			}
			fuzzySetOfObjectsA.add(pom);
		}
		return contextA.closure(fuzzySetOfObjectsA, ClosureType.UP_DOWN);
	} // vrati extent

	private List<List<Double>> leftCorrespondence(List<List<Double>> bond,
			Context contextO, Context contextA) {
		List<List<Double>> result = new ArrayList<List<Double>>();

		for (int i = 0; i < contextO.getObjectsCount(); i++) {
			result.add(contextA.down(bond.get(i)));
		}

		return result;
	}

	private List<List<Double>> rightCorrespondence(List<List<Double>> bond,
			Context contextO, Context contextA) {
		List<List<Double>> result = new ArrayList<List<Double>>();

		List<List<Double>> trBond = transpose(bond);
		for (int i = 0; i < contextA.getAttributesCount(); i++) {
			result.add(contextO.up(trBond.get(i)));
		}

		return result;
	}

	private List<List<Double>> transpose(List<List<Double>> fuzzySets) {
		List<List<Double>> result = new ArrayList<List<Double>>();

		for (int i = 0; i < fuzzySets.get(0).size(); i++) {
			List<Double> temp = new ArrayList<Double>();
			for (int j = 0; j < fuzzySets.size(); j++) {
				temp.add(fuzzySets.get(j).get(i));
			}
			result.add(temp);
		}

		return result;
	}

	private Context directProduct(Context context1, Context context2) {
		Context directProduct = null;
		Integer logicDegree1 = context1.getLogic().getLogicDegree();
		Integer logicDegree2 = context2.getLogic().getLogicDegree();

		if (logicDegree1 != logicDegree2) {
			System.err
					.println("It is not possible to do direct product of contexts with different logic degree");
		}

		logic = new Logic(logicDegree1);

		Double[][] relation1 = context1.getRelation();
		Double[][] relation2 = context2.getRelation();

		Double[][] directProductRelation = new Double[relation1.length
				* relation2[0].length][relation1[0].length * relation2.length];

		int p = 0;
		int q = 0;
		for (int i = 0; i < relation1.length; i++) {
			for (int j = 0; j < relation2[0].length; j++) {
				for (int k = 0; k < relation2.length; k++) {
					for (int l = 0; l < relation1[0].length; l++) {
						directProductRelation[p][q] = logic.disjunction(
								relation1[i][l], relation2[k][j]);
						q++;
					}
				}
				p++;
				q = 0;
			}
		}

		directProduct = new Context(logicDegree1, directProductRelation);

		return directProduct;
	}

	private List<Double> closureOfVectorOfObjects(List<Double> vectorOfObjects,
			Context context, ClosureType closureType) {
		List<Double> closureOfVectorOfObjects = context.closure(
				vectorOfObjects, closureType);
		return closureOfVectorOfObjects;
	}

	private List<Double> getVectorOfObjects(Double[][] relation) {
		Double[] vectorOfObjects = new Double[relation.length
				* relation[0].length];
		int p = 0;
		for (int i = 0; i < relation.length; i++) {
			for (int j = 0; j < relation[0].length; j++) {
				vectorOfObjects[p] = relation[i][j];
				p++;
			}
		}

		return Arrays.asList(vectorOfObjects);
	}

	/**
	 * Metoda spusti vykonavanie algoritmu, ktory vytvori vsetky mozne n-tice
	 * objektovych konceptov a m-tice atributovych konceptov prostrednictvom
	 * kartezianskeho sucinu. Kazda n-tica a m-tica konceptov sa uzavrie pomocou
	 * specialnych uzaverovych operatorov, ktore tvoria Galoisovu konexiu.
	 */
	public void execute() {
		objectTuples = new ArrayList<List<Concept>>();
		attributeTuples = new ArrayList<List<Concept>>();

		Set<?>[] conceptsO = new Set<?>[objectContexts.length];

		for (int i = 0; i < objectContexts.length; i++) {
			Context contextO = objectContexts[i];

			contextO.lattice(ClosureType.UP_DOWN);

			conceptsO[i] = contextO.getConcepts();
		}

		// create tuples from sets of concepts
		Set<Set<Object>> tuplesO = cartesianProduct(conceptsO);

		for (Set<Object> tuple : tuplesO) {
			List<Concept> tupleOfObjectConcepts = new ArrayList<Concept>();
			for (Object concept : tuple) {
				tupleOfObjectConcepts.add((Concept) concept);
			}

			List<Concept> tupleOfAttributeConcepts = specialUp(tupleOfObjectConcepts);

			List<Concept> closedTupleOfObjectConcepts = specialDown(tupleOfAttributeConcepts);

			if (objectTuples.isEmpty()) {
				objectTuples.add(closedTupleOfObjectConcepts);
			} else {
				boolean equal = false;

				for (List<Concept> concepts : objectTuples) {
					if (areTuplesEqual(closedTupleOfObjectConcepts, concepts)) {
						equal = true;
					}
				}
				if (!equal) {
					objectTuples.add(closedTupleOfObjectConcepts);
				}
			}
		}
		System.out.println("number of object concept tuples " + tuplesO.size());
		System.out.println("object tuples size " + objectTuples.size());
		System.out.println("object tuples");
		for (List<Concept> concepts : objectTuples) {
			System.out.print(concepts + " - ");
			System.out.println(specialUp(concepts));
		}
		System.out.println("------------------------------------------");

		// attribute contexts
		Set<?>[] conceptsA = new Set<?>[attributeContexts.length];

		for (int i = 0; i < attributeContexts.length; i++) {
			Context contextA = attributeContexts[i];

			contextA.lattice(ClosureType.UP_DOWN);

			conceptsA[i] = contextA.getConcepts();
		}

		// create tuples from sets of concepts
		Set<Set<Object>> tuplesA = cartesianProduct(conceptsA);

		for (Set<Object> tuple : tuplesA) {
			List<Concept> tupleOfAttributeConcepts = new ArrayList<Concept>();
			for (Object concept : tuple) {
				tupleOfAttributeConcepts.add((Concept) concept);
			}
			List<Concept> tupleOfObjectConcepts = specialDown(tupleOfAttributeConcepts);

			List<Concept> closedTupleOfAttributeConcepts = specialUp(tupleOfObjectConcepts);

			if (attributeTuples.isEmpty()) {
				attributeTuples.add(closedTupleOfAttributeConcepts);
			} else {
				boolean equal = false;
				for (List<Concept> concepts : attributeTuples) {
					if (areTuplesEqual(closedTupleOfAttributeConcepts, concepts)) {
						equal = true;
					}
				}
				if (!equal) {
					attributeTuples.add(closedTupleOfAttributeConcepts);
				}
			}
		}

		System.out.println("number of attribute concept tuples "
				+ tuplesA.size());
		System.out.println("attribute tuples size " + attributeTuples.size());
		System.out.println("attribute tuples");
		for (List<Concept> concepts : attributeTuples) {
			System.out.print(specialDown(concepts) + " - ");
			System.out.println(concepts);
		}
		System.out.println("----------------------------------------------");
		System.out.println();
	}

	private boolean areTuplesEqual(List<Concept> tuple1, List<Concept> tuple2) {
		for (int i = 0; i < tuple1.size(); i++) {
			if (!tuple1.get(i).getExtent().equals(tuple2.get(i).getExtent())) {
				return false;
			}

			if (!tuple1.get(i).getIntent().equals(tuple2.get(i).getIntent())) {
				return false;
			}
		}

		return true;
	}

	/*
	 * cartesian product for an arbitrary number of sets
	 */
	private Set<Set<Object>> cartesianProduct(Set<?>... sets) {
		if (sets.length < 2)
			throw new IllegalArgumentException(
					"Can't have a product of fewer than two sets (got "
							+ sets.length + ")");

		return _cartesianProduct(0, sets);
	}

	private Set<Set<Object>> _cartesianProduct(int index, Set<?>... sets) {
		Set<Set<Object>> ret = new HashSet<Set<Object>>();
		if (index == sets.length) {
			ret.add(new HashSet<Object>());
		} else {
			for (Object obj : sets[index]) {
				for (Set<Object> set : _cartesianProduct(index + 1, sets)) {
					set.add(obj);
					ret.add(set);
				}
			}
		}
		return ret;
	}
}